Загрузка...
 
Печать

DirectX 8 Graphics. Создание и рендеринг объектов


Здесь рассмотрим, как именно Direct3D рисует графику, изучим различные способы применения вершин для рисования полигонов.1 Начнём с вершин.
Для компиляции примеров на понадобится:
  • MS Visual C++ 2010 Express Edition,
  • Microsoft DirectX SDK 8.
Всё легко гуглится + есть в разделе "Софт" нашего сайта.



Применение вершин (Using vertices)

Direct3D позволяет определить вершину множеством различных способов. К примеру, если ты используешь только 2D-графику, можно просто указать координаты 2D экранных (трансформированных) координатах. С другой стороны, если ты используешь локальные (local) и мировые (world) координаты, то можешь указать координаты в 3D-проекции (нетрансформированные координаты). Вдобавок, Direct3D позволяет включать (в том числе) в каждую вершину данные об используемом цвете или текстуре. Как же отслеживать всю эту инфу и быть уверенным, что Direct3D понимает, что ты делаешь в данный момент?
Встречаем гибкий формат вершин (Flexible Vertex Format).

Гибкий формат вершин (Flexible Vertex Format, FVF)

  • Применяется для конструирования кастомных (=пользовательских) структур данных, хранящих инфу о вершинах для дальнейшего использования в (игровых) приложениях.
С FVF ты можешь решать, какую именно информацию будут хранить вершины. Например: 2D или 3D координаты, цвет и т.д. На практике FVF реализуется в виде обычной структуры, в которую можно добавлять компоненты по своему усмотрению. Конечно, здесь есть свои ограничения. Например, компоненты следует указывать в определённом порядке + некоторые компоненты могут конфликтовать друг с другом (например не допускается одновременное указание 2D и 3D координат). После заполнения FVF-структуры создаётся FVF-дескриптор (FVF-descriptor), представляющий собой комбинацию флагов, описывающих твой кастомный формат вершин. Рассмотрим пример FVF-структуры:
Пример FVF-структуры
typedef struct
{
	FLOAT x, y, z, rhw; // 2D-координаты
	FLOAT x, y, z; // 3D-координаты
	FLOAT nx, ny, nz; // Нормали
	D3DCOLOR diffuse; // Рассеянный (diffuse) цвет
	FLOAT u, v;	// Координаты текстуры
} sVertex;

Данный код содержит структуру вершины (vertex structure), заполненную различными переменными, допустимыми форматом FVF. Порядок их указания важен! При удалении каких-либо переменных из данного примера необходимо сохранить порядок их перечисления. Как видим, единственный конфликт здесь возникает при указании 2D и 3D координат + координат нормалей. Нормали (normals) представляют собой координаты, определяющие направление. Они могут использоваться только совместно с 3D-координатами. Здесь необходимо выбрать, какие координаты (2D или 3D) выбрать, а от каких отказаться. Оба вида координат в одной FVF-структуре использовать нельзя.
Единственное отличие 2D и 3D координат - присутствие у 2D-координат отдельного параметра rhw (reciprocal of the homogeneous W; обратная величина однородного (в пространстве отсечения) W). Если в двух словах, то rhw обычно представляет собой расстояние от наблюдателя (viewpoint) к вершине, расположенной на оси Z. В большинстве случаев можно выставить rhw в 1,0 и не париться.
Обрати внимание на то, что пользовательская структура sVertex активно использует тип данных FLOAT (значение с плавающей точкой).
Тип данных D3DCOLOR - это по сути переменная типа DWORD, которая применяется в Direct3D для хранения цветовых значений.
Для создания цветового значения типа D3DCOLOR применяют одну из двух функций:
D3DCOLOR D3DCOLOR_RGBA(Red, Green, Blue, Alpha);
D3DCOLOR D3DCOLOR_COLORVALUE(Red, Green, Blue, Alpha);

Каждая из этих функций (вообще, это макросы) принимает 4 параметра, представляющие собой количественные значения (amount) каждого цветового компонента (color component) + цвет альфа-канала (канала прозрачности). Данные значения варьируются:
  • от 0 до 255 для каждого параметра макроса D3DCOLOR_RGBA,
  • от 0.0 до 1.0 для параметров макроса D3DCOLOR_COLORVALUE.
Для цвета альфа-канала можно выбрать любое допустимое значение. Но на практике его обычно выставляют в макс. значение 255 (или 1.0).

Создаём пользовательскую FVF-структуру (пример)

Например, нам необходимо включить в кастомную FVF-структуру только 3D-координаты вершины и данные по её рассеянному цвету (diffuse color):
typedef struct
{
	FLOAT x, y, z;
	D3DCOLOR diffuse;
}sVertex;


Создаём FVF-дескриптор

Как только FVF-структура создана, создаём её FVF-дескриптор, указав один или несколько специальных флагов:
ФЛАГ ОПИСАНИЕ
D3DFVF_XYZ В структуру включены 3D-координаты вершины.
D3DFVF_XYZRHW В структуру включены 2D-координаты вершины.
D3DFVF_NORMAL В структуру включены данные нормали (вектора нормали).
D3DFVF_DIFFUSE В структуру включены данные о рассеянном (diffuse) цвете вершины.
D3DFVF_TEXT В структуру включены текстурные координаты.

В нашем случае определение дескриптора выглядит так:
#define VertexFVF (D3DFVF XYZ | D3DFVF DIFFUSE)

Указаны только флаги, необходимые нашей FVF-структуре.

Вершинные буферы (Vertex Buffers)

Image
Рис.1 Треугольный список (list) не имеет общих вершин с другими полигонами. Объединяя полигоны в треугольные полосы (strips) и вентиляторы (fans), мы сокращаем число вершин, тем самым экономя ресурсы и увеличивая производительность

После создания FVF-структуры вершины и её дескриптора, создаём объект, содержащий массив (array) вершин. Для выполнения этой задачи Direct3D (DirectX 8) предлагает два шаблонных объекта :
  • IDirect3DVertexBuffer8
  • IDirect3DIndexBuffer8.
В данной главе будем применять только объект IDirect3DVertexBuffer8, который хранит вершины для рисования базовых Direct3D-примитивов (См. Рис.1):
  • Список треугольников (triangle list);
  • Полоса треугольников (triangle strip);
  • Вентилятор треугольников (triangle fan).
Подробнее о данных примитивах читай здесь: Базовые понятия 3D-графики.
При создании списка треугольников (triangle list) объект IDirect3DVertexBuffer8 хранит как минимум 3 вершины для каждого отрисовываемого полигона (напомним, вершины при этом указываются по часовой стрелке). При создании полосы треугольников (triangle strip) первый полигон образуется тремя вершинами, а последующие - путём добавления в пространстве ещё одной вершины. Что касается вентилятора треугольников (triangle fan), то здесь сначала сохраняется всего 1 вершина, а каждый полигон дорисовывается, путём добавления ещё двух вершин.
Закрыть
noteОбрати внимание

С точки зрения Direct3D полигон может состоять из 3-х, 2-х (отрезок) и даже из одной вершины, в зависимости от того, что ты рисуешь. В то же время в классической геометрии грань фигуры может состоять из более чем трёх вершин (например гранью куба является квадрат, у которого 4 вершины). В терминологии 3D-моделирования принято приравнивать грань фигуры к полигону, который в большинстве систем состоит ровно из трёх вершин. Системы, оперирующие четырёхугольными полигонами встречаются нечасто. Одной из таких была игровая консоль Sega Saturn, увидевшая свет в 1995 г. Её создатели хотели как лучше, а на деле лишь прибавили головной боли разработчикам игр, которые ранее никогда не работали с четырёхугольными полигонами. В данной статье рассматриваются только треугольные полигоны.


Создаём вершинный буфер (Vertex Buffer; DirectX 8) и его дескриптор

Вершинный буфер создаётся на базе предварительно проинициализированного объекта IDirect3DDevice8. С помощью функции CreateVertexBuffer:
Прототип функции CreateVertexBuffer
HRESULT IDirect3DDevice8::CreateVertexBuffer (
	UINT Lenght, // Число байт, задействованное под все FVF-структуры.
	DWORD Usage, // Выставляем D3DCREATE_WRITEONLY
	DWORD FVF, // Дескриптор FVF
	D3DPOOL Pool, // Выставялем D3DPOOL_MANAGED
	IDirect3DVertexBuffer **ppVertexBuffer // Указатель на вершинный буфер.
	);

Здесь единственный изменяемый параметр - это флаг Usage. Обычно здесь всегда стоит значение D3DCREATE_WRITEONLY. Изменяют его очень редко. Другой возможный вариант данного флага - D3DCREATE_SOFTWAREPROCESSING. Ранее в этой статье мы создали пользовательскую FVF-структуру, включающую в себя 3D-координаты и данные по diffuse-цвету вершины, а также прописали её (струкутры) дескриптор. Выглядело это так:
#define VertexFVF (D3DFVF_XYZ | D3DFVF_DIFFUSE)
typedef struct
{
	FLOAT x, y, z;
	D3DCOLOR diffuse;
}sVertex;

Создадим на основе данной структуры вершинный буфер, содержащий 4 вершины:
// g_pD3DDevice - предварительно проинициализированный объект устройства.
// sVertex - созданная в предыдущем примере пользовательская FVF-структура.
// VertexFVF - дескриптор FVF-структуры
IDirect3DVertexBuffer8 *pD3DVB = NULL;

// Создаём вершинный буфер
if(FAILED(g_pD3DDevice->CreateVertexBuffer(sizeof(sVertex) * 4,
	D3DCREATEWRITEONLY, VertexFVF, D3DPOOL_MANAGED, &pD3DVB)))
{
	// Ошибка.
}

Закрыть
noteОбрати внимание

По завершении работы с вершинным буфером не забудь его удалить путём вызова макроса RELEASE.


Блокируем вершинный буфер (Locking the Vertex Buffer)

Перед добавлением вершин в вершинный буфер, необходимо сперва заблокировать (lock) память, которую этот самый буфер использует. Это сделает участок памяти с вершинами доступным для редактирования. С помощью функции Lock мы получаем доступ к памяти вершинного буфера и извлекаем (retrieve) его (буфера) указатель. Вот прототип функции IDirect3DVertexBuffer8::Lock:
Прототип функции Lock
HRESULT IDirect3DVertexBuffer8::Lock (
	UINT OffsetToLock, // Смещение блокировки буфера в байтах.
	UINT SizeToLock, // Сколько байт залочить (0 - все байты).
	BYTE** ppbData. // Указатель на указатель (для доступа к данным буфера).
	DWORD Flags // 0
	);

Пример её применения:
// pD3DVB - предварительно проинициализированный вершинный буфер.
BYTE *Ptr;

// Блокируем (Lock) память вершинного буфера и получаем указатель на него:
if(FAILED(pD3DVB->Lock(0, 0, (BYTE**)&Ptr, 0)))
{
	// Ошибка.
}

По завершении работы с вершинным буфером каждый залоченный буфер разблокируем отдельной командой Unlock:
HRESULT IDirect3DVertexBuffer8::Unlock();

Вызывается без параметров. Очень важно, чтобы на каждую команду Lock приходилась своя команда Unlock. То есть в одном исходнике число вызовов команды Lock должно совпадать с числом вызовов команды Unlock.

Заполняем вершинный буфер вершинами

Итак, на данном этапе мы:
  • Создали собственную FVF-структуру для хранения данных вершин.
  • Создали дескриптор по её применению.
  • Создали на основе нашей FVF-структуры вершинный буфер.
  • Заблокировали (Lock) его, подготовив для заполнения данными.
Пробил час, собственно, заполнить буфер данными по четырём вершинам. В свете того, что при выполнении функции Lock мы получили указатель на участок памяти вершинного буфера, нам остаётся лишь скопировать (подходящие под формат FVF-структуры) вершины в вершинный буфер.
Для примера предварительно создадим набор из четырёх вершин, представив его в виде массива:
sVertex Verts[4] = {
	{-100.0f, 100.0f, 100.0f, D3DCOLOR_RGBA(2 55, 255, 255, 255)},
	{100.0f, 100.0f, 100.0f, D3DCOLOR_RGBA(255, 0, 0, 255)},
	{100.0f, -100.0f, 100.0f, D3DCOLOR_RGBA(0, 255, 0, 255)},
	{-100.0f, -100.0f, 100.0f, D3DCOLOR_RGBA(0, 0, 255, 255)}
};

Теперь мы блокируем (Lock) вершинный буфер, получая указатель на его участок памяти, и затем копируем вершины из массива:
// pD3DVB - предварительно проинициализированный вершинный буфер. BYTE *Ptr;
// Блокируем вершинный буфер и получаем указатель на него.
if(SUCCEEDED(pD3DVB->Lock(0, 0, (BYTE**)&Ptr, 0)))
{
	// Копируем массив с вершинами Verts в вершинный буфер
	memcpy(Ptr, Verts, sizrof(Verts));
}

По завершении мы разблокируем (Unlock) вершинный буфер:
pD3DVB->Unlock();

Готово! Вершинный буфер создан и заполнен вершинами.
Следующий шаг - назначить ему (вершинному буферу) отдельный поток-источник (stream source) и вершинный шейдер (vertex shader).

Вершинные потоки (Vertex Streams). Функция SetStreamSource (DirectX 8)

Direct3D (начиная с версии 8) позволяет отправлять вершины рендереру посредством нескольких потоков, называемых вершинными потоками (vertex streams). Данная тема весьма обширна и инфу по ней нетрудно найти инете (почти вся на англ. языке). Для простоты изложения в данной статье мы будем использовать всего 1 поток.
Для привязки заполненного вершинного буфера определённому потоку данных устройства (device data stream) применяют функцию IDirect3DDevice8::SetStreamSource:
Прототип функции IDirect3DDevice8::SetStreamSource
HRESULT IDirect3DDevice8::SetStreamSource(
	UINT StreamNumber, // 0
	IDirect3DVertexBuffer8* pStreamData, // Объект вершинного буфера.
	UINT Stride // Размер FVF-структуры вершины.
	);

При успешном выполнении возвращаемое значение D3D_OK. В случае неудачи возвращается D3DERR_INVALIDCALL или другие значения.2
Здесь:
  • StreamNumber - номер потока. 0 - первый поток, -1 - максимально возможный номер потока.
  • Stride (от англ. "шаг") - шаг компонента (в байтах).
При использовании FVF-структуры шаг вершинного потока (stream vertex stride) должен совпадать с размером вершины, заданным её FVF-структурой. В нашем случае её вызов выглядит так:
// g_pD3DDevice - объект устройства Direct3D.
// pD3DVB - предварительно проинициализированный вершинный буфер.
if(FAILED(g_pD3DDevice->SetStreamSource(0, pD3DVB, sizeof(sVertex))))
{
	// Ошибка
}

Здесь в параметре Stride мы указали размер всего массива sVertex.

Вершинные потоки (Vertex Streams). Функция SetStreamSource (DirectX 9)

Функция IDirect3DDevice9::SetStreamSource привязывает вершинные буферы к потокам данных устройства:3
Прототитп функции IDirect3DDevice9::SetStreamSource
HRESULT IDirect3DDevice9::SetStreamSource(
	UINT StreamNumber,
	IDirect3DVertexBuffer9 * pStreamData,
	UINT OffsetInBytes,
	UINT Stride
);

Здесь:
  • StreamNumber - Номер потока. На современных видеокартах их поддерживается по несколько штук.
  • pStreamData - Указатель на интерфейс вершинного буфера, который будет привязан к потоку.
  • OffsetInBytes - смещение от начала потока к началу вершинных данных. Задаётся в байтах.
  • Stride. Данный параметр должен быть равным размеру (занимаемому объёму памяти) вершины либо размеру её FVF-структуры.

Вершинные шейдеры (Vertex Shaders; DirectX 8)

  • Это механизм, обеспечивающий загрузку и обработку вершин;
  • Включают в себя изменение координат вершин, применение цвета и т.н. затуманивания (fogging) + ряд других операций.
  • Бывают двух видов: фиксированный (fixed; предустановленный; здесь весь необходимый функционал встроен в него) и программируемый (programmable; здесь любую процедуру можно изменять).
По теме программируемых шейдеров написана не одна сотня книг. Поэтому для простоты изложения в данной статье мы будем применять только фиксированные вершинные шейдеры. Обычно их функционала хватает слихвой. Так вот. Для назначения вершинам вершинного шейдера применяют функцию IDirect3DDevice8::SetVertexShader:
HRESULT IDirect3DDevice8::SetVertexShader(DWORD Handle); // Указатель на кастомную (или не очень) FVF-структуру для хранения вершин.

В нашем случае пример её применения выглядит так:
// g_pD3DDevice8 - предварительно проинициализированный объект устройства Direct3D.
// VertexFVF - предварительно созданный дескриптор на кастомную FVF-структуру/
if(FAILED(g_pD3DDevice->SetVertexShader(VertexFVF)))
{
	// Ошибка.
}

Здесь мы указали вершинному шейдеру используемый формат FVF-структуры. Следующий шаг - назначить различные трансформации, необходимые для позиционирования вершин (изначально расположенных в локальном пространстве) в мировое пространство. Само собой, всё нижеизложенное справедливо только для 3D-координат!

Трансформации (Transformations)

Когда имеешь дело с 3Р-объектами (вершины, полиогоны и т.д.), изначально они создаются в локальной системе координат (local space). Для корректного позиционирования при рендеринге они предварительно должны пройти через несколько матриц преобразований (мировая, вида и проекции). Подробнее об этом читай раздел "Трансформации" в статье Базовые понятия 3D-графики.
Каждая трансформация требует создание специальной матрицы (всего их 3), которая представляет соответствующие значения ориентации или проекции вершин/объектов в пространстве. Рассмотрим каждую из этих матриц подробнее.

Мировая трансформация (The World Transformation)

Image
Рис.2 Объект, созданный в локальном пространстве, перед рендерингом должен быть сориентирован в мировом пространстве посредством мировой матрицы трансформации

Вершины, изначально определённые в локальном пространстве, должны быть сориентированы в мировом. К примеру, ты создал бокс (box, куб) из вершин в локальном пространстве и хочешь разместить его в определённом месте в мире. Для этого к соответствующим координатам каждой его вершины необходимо применить мировую матрицу (См. Рис.2). Для её создания применяют вспомогательную библиотеку D3DX (DirectX 8). Только что созданная мировая матрица (World matrix) сама по себе бесполезна. Перед применением к вершинам она последовательно комбинируется (=перемножается) с несколькими другими матрицами, отражающими положение объекта в 3D-пространстве:
  • матрица вращения по оси X (X Rotation matrix);
  • матрица вращения по оси Y (Y Rotation matrix);
  • матрица вращения по оси Z (Z Rotation matrix);
  • матрица трансляции (=перемещения; Translation matrix);
  • матрица масштабирования (Scale matrix).
Создаётся всё это так:
D3DXMATRIX matWorld; // Результирующая матрица мира.
D3DXMATRIX matRotX, matRotY, matRotZ;
D3DXMATRIX matTrans;
D3DXMATRIX matScale;

// Создаём матрицы вращения. По одной на каждую ось.
D3DXMatrixRotationX(&matRotX, XAngle);
D3DXMatrixRotationY(&matRotY, XAngle);
D3DXMatrixRotationZ(&matRotZ, XAngle);

// Создаём матрицу трансляции (=перемещения).
D3DXMatrixTranslation(&matTrans, XPos, YPosm ZPos);

// Создаём матрицу масштабирования.
D3DXMatrixScaling(&matScale, XScale, YScale, ZScale);

Далее просто комбинируем (=перемножаем) 3 матрицы вращения, матрицы трансляции и масштабирования в одну мировую матрицу (World matrix).
Закрыть
noteВажно!

Для создания корректной мировой матрицы её нужно комбинировать с матрицами-компонентами в строго определённом порядке:
  1. матрица масштаба (Scale matrix),
  2. матрица вращения по оси X,
  3. матрица вращения по оси Y,
  4. матрица вращения по оси Z,
  5. матрица трансляции (Translation matrix).

В коде это выглядит так:
// Задаём матрицу matWorld в качестве единичной (identity matrix).
D3DXMatrixIdentity(SmatWorld);

// Поочерёдно комбинируем все матрицы-компоненты с мировой матрицей трансформации.
D3DXMatrixMultiply(SmatWorld, SmatWorld, SmatScale);
D3DXMatrixMultiply(SmatWorld, SmatWorld, SmatRotX);
D3DXMatrixMultiply(SmatWorld, SmatWorld, SmatRotY);
D3DXMatrixMultiply(SmatWorld, SmatWorld, SmatRotZ);
D3DXMatrixMultiply(SmatWorld, SmatWorld, SmatTrans);

Мы почти закончили. Осталось только указать Direct3D использовать полученную матрицу в качестве мировой путём вызова функции SetTransform. Вот её прототип:
Прототип функции SetTransform
HRESULT IDirect3DDevice8::SetTransform(
	D3DTRANSFORMSTATETYPE State, // Обычно здесь стоит D3DTS_WORLD.
	CONST D3DMATRIX *pMatrix // Устанавливаем указанную мировую матрицу.
	);

Здесь второй параметр является указателем на структуру D3DMATRIX. Но в нашем случае сюда можно подставить указатель на созданную выше результирующую мировую матрицу.
Установка первого параметра в D3DTS_WORLD говорит Ditect3D, что выбранная матрица используется для мировой трансформации (world transformation). И всё, что будет отрисовываться после этого, должно быть ориентировано данной мировой матрицей.
Если у нас два и более объектов, которые необходимо сориентировать в мировом пространстве, то на каждый из них надо создавать отдельную мировую матрицу (в соответствии с текущим положением данных объектов в пространстве) и затем так же по отдельности на каждый объект вызывать отдельную функцию SetTransform, назначающую объекту конкретную мировую матрицу.

Трансформация вида (The View Transformation)

Если кратко, то трансформация вида выполняет роль виртуальной камеры (часто её называют вьюпортом, от англ. "viewport"). Создав матрицу, содержащую смещения ранее установленных мировых координат объектов, мы размещаем сцену с объектами перед "объективом" вьюпорта. С помощью трансформации вида все вершины должны быть сориентированы вокруг центра мира точно так же, как и их позиция относительно вьюпорта.
Для создания трансформации вида создают матрицу, отражающую текущую позицию и поворот вьюпорта. Здесь порядок комбинирования матриц-компонентов с матрицей вида следующий:
  1. матрица трансляции,
  2. матрица вращения по оси Z,
  3. матрица вращения по оси Y,
  4. матрица вращения по оси X.
Но здесь есть одна тонкость. При создании матрицы трансформации вида значения элементов матриц-компонентов должны быть отрицательными. Например при текущей позиции вьюпорта x=10, y=0, z=150 значения элементов матриц-компонентов будут следующими: x=-10, y=0, z=-150. Код создания результирующей матрицы вида выглядит так:
D3DXMATRIX matView;

D3DXMATRIX matRotX, matRotY, matRotZ;
D3DXMATRIX matTrans;

// Создаём матрицы вращения (с отрицательными значениями).
D3DXMatrixRotationX(&matRotX, -XAngle);
D3DXMatrixRotationY(&matRotY, -YAngle);
D3DXMatrixRotationZ(&matRotZ, -ZAngle);

// Создаём матрицу трансляции (с отрицательными значениями).
D3DXMatrixTranslation(&matTrans, -XPosm -YPos, -ZPos);

// Устанавливаем матрицу вида в качестве единичной.
D3DMatrixIdentity(&matView);

// Поочерёдно комбинируем каждую из матриц-компонентов с матрицей (трансформации) вида.
D3DXMatrixMultiply(&matView, &matView, &matTrans);

D3DXMatrixMultiply(&matView, &matView, &matRotZ);
D3DXMatrixMultiply(&matView, &matView, &matRotY);
D3DXMatrixMultiply(&matView, &matView, &matRotX);

Перед началом использования результирующей матрицы вида назначим её с помощью всё той же функции SetTransform. В этот раз первый параметр выставляем в D3DTS_VIEW:
// g_pD3DDevice - предварительно проинициализированный объект устрйоства Direct3D.

if(FAILED(g_pD3DDevice->SetTransform(D3DTS_VIEW, &matView)))
{
	// Ошибка.
}

Как видишь, назначить матрицу вида вершинам совсем не трудно. Куда сложнее её создать. Для упрощения данного процесса в D3DX есть отличная функция D3DXMatrixLookAtLH, которая создаёт матрицу вида буквально за один вызов. Вот её прототип:
Прототип функции D3DXMatrixLookAtLH
D3DXMATRIX* D3DXMatrixLookAtLH(
	D3DXMATRIX* pOut, // Результирующая (output) матрица вида.
	CONST D3DXVECTOR3* pEye, // Координаты вьюпорта.
	CONST D3DXVECTOR3* pAt, // Координаты точки наблюдения (по направлению луча "зрения").
	CONST D3DXVECTOR3* pUp // Определяет, где у нас вверх.
	);

В коде видим незнакомые объекты D3DXVECTOR3. На деле они очень схожи с объектами типа D3DXMATRIX с той лишь разницей, что их векторные "собратья" содержат всего 3 элемента. В нашем случае - 3 координаты. Объект D3DXVECTOR3 так и называют - векторный объект.
Вектор pEye представляет собой координаты самого вьюпорта. pAt - координаты цели, куда устремлён "взгляд" вьюпорта. pUp - это вектор, указыващий, в какой стороне у вьюпорта находится верх. Обычно его устанавливают в (0,1,0), что означает положительное направление оси Y. Но, в свете того, что вьюпорт может вращаться в разные стороны (подобно тому, как обычный наблюдатель может поворачивать свою голову из стороны в сторону), направление "вверх" можно выбирать любое и по любой из осей (в зависимости от игры). Вот пример использования функции D3DXMatrixLookAtLH:
D3DXMATRIX matView;
D3DXVECTOR3 vecVP, vecTP, vecUp(0.0f, 1.0f, 0.0f);
vecVP.x = XPos; vecVP.y = YPos; vecVP.z = ZPos;
vecTP.x = vecTP.y = vecTP.z = 0.0f;

D3DXMatrixLookAtLH(SmatView, SvecVP, SvecTP, SvecUp);

Здесь предполагается, что вьюпорт находится в точке с координатами XPos, YPos, ZPos и что смотрит он в начало координат.

Трансформация проекции (The Projection transformation)


Image
Рис.3 Трансформация проекции даёт возможность увидеть объекты на плоском экране монитора

Image
Рис.4 Усечённая пирамида (frustum) видимого пространства. Источник: https://obviam.github.io

Image
Рис.5 Трансформация проекции учитывает соотношение сторон т.н. усечённой пирамиды вида, а также её ближнюю и дальнюю плоскости отсечения.


Завершает череду преобразований трансформация проекции, которая преобразует (нетрансформированные) 3D-координаты вершин в (трансформированные) 2D-координаты. На основе последних Direct3D выводит графику на экран. Представь, будто трансформация проекции "сплющивает" 3D-изображение и выводит на проский экран монитора (См. Рис.3). Здесь вступают в "игру" следующие аспекты (См. Рис.4):
  • Соотношение сторон вьюпорта (Aspect Ratio of viewport);
  • Поле видимости (Field of View, FOV);
  • Ближняя плоскость отсечения (Near clipping plane);
  • Дальняя плоскость отсечения (Far clipping plane).
Пара слов о плоскостях отсечения. При 3D-рендеринге объекты часто расположены слишком близко либо слишком далеко от вьюера. В целях оптимизации мы просто указываем Direct3D просто отсекать (clip out) их. Для создания матрицы проекции и определении видимой области (внутри которой не отсекаются) применяем функцию D3DXMatrixPerspectiveFovLH. Вот её прототип:
Прототип функции D3DMatrixPerspectiveFovLH
D3DXMATRIX* D3DMatrixPerspectiveFovLH(
	D3DXMATRIX* pOut, // Результирующая матрица.
	FLOAT fovy, // Поле видимости, в радианах.
	FLOAT Aspect, // Соотношение сторон (Aspect Ratio)
	FLOAT zn, // Z-значение ближней (near) плоскости отсечения.
	FLOAT zf // Z-значение дальней (far) плоскости отсечения.
	);

Параметр fovy указывает ширину вида проекции. Т.е. чем больше значение, тем больше объектов сцены видно в кадре одновременно. В случае слишком больших или слишком малых значений возникает эффект искажения краёв кадра (т.н. линза "рыбий глаз"). Типичное значение здесь D3DX_PI/4, что соответствует 1/4 числа пи.
Параметр Aspect представляет собой соотношение сторон видимой области. Если игра идёт в окне 400х400, то Apect выставляем 1:1 или 1:0 (т.к. окно квадратное). В случае окна 400х200 выставляем Aspect в 2:1 или 2:0. Для расчёта соотношения сторон ширину окна (в пикселях) делят на его высоту. Иногда просто применяют формулу с переменными, вроде этой:
FLOAT Aspect = (FLOAT)WindowWidth / (FLOAT)WindowHeight;

Параметры zn и zf представляют собой значения для ближней и дальней плоскостей отсечения (clipping plane) и измеряются в тех же самых единицах, что определены в FVF-формате 3D-вершин. Типичные значения:
  • для параметра zn: 1.0;
  • для параметра zf: 1000.0 .
Эти значения означают, что полигоны, ближе чем 1.0 единиц и дальше чем 1000.0 единиц не рендерятся.
После создания матрицы проекции, она устанавливается в качестве таковой спецфункцией IDirect3DDevice8::SetTransform, где в качестве параметра STATE указываем D3DTS_PROJECTION. Вот пример:
// g_pD3DDevice - предварительно проинициализированный объект устройства Direct3D.
D3DXMATRIX matProj;

// Создаём матрицу проекции.
D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI/4, 1.0f, 1.0f, 1000.0f);

// Назначаем созданную матрицу в качестве матрицы проекции.
if(FAILED(g_pD3DDevice->SetTransform(D3DTS_PROJECTION, &matProj)))
{
	// Ошибка.
}


Цвета и материалы (Colors and Materials; DirectX 8)


В начале этой статьи, при изучении гибкого формата вершин (FVF), мы определяли цвет как один из параметров вершины. В отличие от вершин, полигоны имеют дополнительные цветовые атрибуты.
Закрыть
noteЗапомни!

Цвет, применяемый к полигону, называется материалом (material).

Перед рендерингом полигона средствами Direct3D программер может назначить ему материал. Если материал не назначен, то полигон окрашивается в цвета своих вершин (if any).
каждый материал содержит несколько цветовых значений, описывающих его. В Direct3D цветовые значения материала хранятся в специальной структуре D3DMATERIAL8:
typedef struct _D3DMATERIAL8
{
	D3DCOLORVALUE Diffuse; // Цветовой компонент рассеяного (diffuse) света.
	D3DCOLORVALUE Ambient; // Цветовой компонент света подсветки (Ambient).
	D3DCOLORVALUE Specular; // Цветовой компонент блика (Specular).
	D3DCOLORVALUE Emissive; // Цветовой компонент излучаемого света (Emissive).
	float Power; // Чёткость бликов.
} D3DMATERIAL8;

На практике:
  • В большинстве случаев имеют дело лишь с одним компонентом - Diffuse.
  • Компоненту Ambient часто присваивают то же значение, что и у компонента Diffuse.
  • Компонент Specular изменяется от 0.0 до 1.0 (при Power = 0.0).
Желательно "поиграть" со значениями данных параметров, чтобы разобраться в них.
В нашем случае мы применяем к грани (face) полигона Diffuse-компонент. Если применить цвет материала к полигону, у которого уже заданы цвета вершин, то можно заметить (часто нежелательные) изменения в цвете полигона. Поэтому лучше использовать что-то одно: либо цвета вершин, либо цвет материала полигона.
При работе с цветовыми компонентами материала, цвета каждого компонента присваиваются напрямую, без использования макроса D3DCOLOR_RGBA. При этом каждому цветовому компоненту компонента материала соответствует своя буква, по цвету: r - red (красный), g - green (зелёный), b - blue (синий), a - alpha (цвет прозрачности). Например, при создании материала жёлтого цвета структура материала может выглядеть так:
D3DMATERIAL8 d3dm;

// Очищаем структуру материала.
ZeroMemory(&d3dm, sizeof(D3DMATERIAL8));

// Заполняем компоненты Diffuse и Ambient жёлтым цветом.
d3dm.Diffuse.r = d3dm.Ambient.r = 1.0f;	//	r - red (красный)
d3dm.Diffuse.g = d3dm.Ambient.g = 1.0f;	//	g - green (зелёный)
d3dm.Diffuse.b = d3dm.Ambient.b = 0.0f;	//	b - blue (синий)
d3dm.Diffuse.a = d3dm.Ambient.a = a.0f;	//	a - alpha (альфа-компонент; цвет прозрачности)

Как только структура материала создана, необходимо указать Direct3D использовать её при рендеринге полигона с помощью функции IDirect3DDevice8::SetMaterial, принимающей всего 1 параметр - указатель на созданную структуру материала:
IDirect3DDevice8::SetMaterial(CONST D3DMATERIAL8 *pMaterial);

После единственного её вызова все полигоны будут рендериться только с этим материалом. В нашем случае созданная выше структура материала назначается так:
g_pD3DDevice->SetMaterial(&d3dm);

Очищаем вьюпорт (Clearing the viewport; DirectX 8)
Для подготовки бэкбуфера к рендерингу его необходимо очистить (например от графики, которая осталась в нём от рендеринга предыдущего кадра). Делается это с помощью функции IDirect3DDevice8::Clear:
HRESULT IDirect3DDevice8::Clear(
	DWORD Count, // 0
	CONST D3DRECT* pRects, // NULL
	DWORD Flags, // D3DCLEAR_TARGET
	D3DCOLOR Color, // Цвет, в который всё заливаем при очистке.
	float Z, // 1.0f
	DWORD Stencil  // 0
	);

Единственный параметр, который представляет для нас интерес - Color. Он задаёт цвет, в который зальётся бэкбуфер при очистке. Здесь можно применять уже знакомые макросы D3DCOLOR_RGBA или D3DCOLOR_COLORVALUE. Допустим, нам надо очистить бэкбуфер, залив его светло-голубым цветом:
// g_pD3DDevice - предварительно проинициализированный объект устройства Direct3D.

if(FAILED(g_pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET,
	D3DCOLOR_RGBA(0,0,192,255), 1.0f, 0))
{
	// Ошибка
}


Начало и конец прорисовки сцены (Beginning and Ending the Scene; DirectX 8)

Прежде чем что-либо отрендерить, необходимо сперва скомандовать Direct3D подготовиться к прорисовке с помощью функции IDirect3DDevice8::BeginScene, не имеющей параметров:
HRESULT IDirect3DDevice8::BeginScene();

По окончании рендеринга сцены информируем Dierct3D о том, что мы закончили прорисовку:
HRESULT IDirect3DDevice8::EndScene();

Между вызовами функций BeginScene и EndScene нельзя включать функцию Clear. Обычно это делают перед вызовом функции BeginScene.
Единственное, что может находиться между вызовами функций BeginScene и EndScene - это вызовы функции, непосредственно рендерящие полигоны.

Рендеринг полигонов (Rendering Polygons; DirectX 8)

Наконец-то можно рендерить полигоны! Типичный кадр игры проделывает следующие операции:
  • очищает бэкбуфер;
  • начинает рендеринг сцены (BeginScene);
  • назначает материал;
  • рендерит полигоны;
  • заканчивает рендеринг сцены (EndScene).
Выше описаны все эти пункты, за исключением самого рендеринга. Готовый вершинный буфер (объект IDirect3DVertexBuffer8 должен быть к этому моменту заполнен и ему должны быть назначены вершинный поток (vertex stream) и шейдер (shader)) рендерится путём вызова функции DrawPrimitive:
Прототип функции DrawPrimitive
HRESULT IDirect3DDevice8::DrawPrimitive(
	D3DPRIMITIVETYPE PrimitiveType, // Тип примитива.
	UINT StartVertex, // Вершина, с которой начинаем прорисовку (обычно 0).
	UINT PrimitiveCount  // Кол-во примитивов.
	);

Параметр PrimitiveType может принимать одно из следующих значений (См. Рис.1):
ЗНАЧЕНИЕ ОПИСАНИЕ
D3DPT_POINTLIST Рисует все вершины в виде отдельных пикселей на экране.
D3DPT_LINELIST Рисует все вершины в виде отдельных отрезков на экране.
D3DPT_LINESTRIP Рисует ломаную линию, состоящую из отрезков, соединённых друг с другом.
D3DPT_TRIANGLELIST Рисует отдельные полигоны, используя по 3 вершины на каждый из них.
D3DPT_TRIANGLESTRIP Рисует ленту (strip) из полигонов. Первый полигон строится из 3 вершин. Последующие полигоны строятся путём добавления 1 вершины к ребру предыдущего полигона.
D3DPT_TRIANGLEFAN Рисует полигоны в виде вентилятора. Одна из вершин первого полигона является общей для всех последующих.

Параметр PrimitiveCount устанавливается в общее число примитивов (полигонов), которое хотим нарисовать. Вызов функции DrawPrimitive обязательно обрамляется функциями BeginScene и EndScene. Иначе вызов DrawPrimitive вызовет ошибку.
Допустим, у нас есть вершинный буфер с 6 вершинами, из которого выстраиваем 2 треугольных полигона в форме квадрата. Вот пример такого построения:
// g_pD3DDevice - предварительно проинициализированный объект устройства Direct3D.
// pD3DVB - готовый вершинный буфер.

// sVertex - предварительно созданная FVF-структура (гибкого формата вершин).
// VertexFVF - FVF-дескриптор.

// Назначаем вершинный поток (vertex stream) и шейдер (shader).
g_pD3DDevice->SetStreamSource(0, pD3DVB, sizeof(sVertex));
g_pD3DDevice_>SetVertexShader(VertexFVF);

if(SUCCEEDED(g_pD3DDevice->BeginScene ()))
{
	// Рендерим полигоны.
	if(FAILED(g_pD3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2)))
	{
		// Ошибка.
	}
}

// Заканчиваем прорисовку.
g_pD3DDevice->EndScene();


Вывод сцены на экран (Presenting Scene; DirectX 8)

  • Завершающий шаг.
  • Подразумевает смену (flip) буферов, в результате которого подготовленный бэкбуфер с полигонами выводится на экран.
  • Выполняется функцией IDirect3DDevice8::Present.
Вот прототип данной функции:
HRESULT IDirect3DDevice8::Present(
	CONST RECT* pSourceRect,
	CONST RECT* pDestRect,
	HWND hDestWindowOverride,
	CONST RGNDATA* pDirtyRegion
	);

Здесь можно смело выставить все параметры в NULL, тем самым указав Direct3D обновить весь текущий экран. Другие параметры нужны для вывода изображения небольшими порциями. Вот пример её применения:
// g_pD3DDevice - предварительно проинициализированный объект устройства Direct3D.

if(FAILED(g_pD3DDevice->Present(NULL, NULL, NULL, NULL)))
{
	// Ошибка.
}

Для создания более реалистичных сцен используй несколько различных вершинных буферов (по одному на каждый 3D-объект сцены).

Источники


1. Adams J. Programming Role Playing Games with DirectX 8.0. - Premier Press, 2002
2. Документация DirectX SDK 8.1


Последние изменения страницы Вторник 24 / Май, 2022 23:19:40 MSK

Последние комментарии wiki

No records to display

Search Wiki Page

Точное совпадение

Категории

|--> C#
|--> C++